package com.mapperdb.builder.annotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.ResultMap;
import org.apache.ibatis.annotations.SelectKey;
import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.builder.BuilderException;
import org.apache.ibatis.builder.IncompleteElementException;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultSetType;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;

import com.mapperdb.helper.Annt;
import com.mapperdb.helper.AnntHelper;
import com.mapperdb.mapping.EmptySqlSource;
import com.mapperdb.util.MapperUtils;

public abstract class AbstractAnntBuilder implements AnntBuilder {

	protected Configuration configuration;
	protected MapperBuilderAssistant assistant;
	protected Class<?> type;

	public AbstractAnntBuilder(Configuration configuration, Class<?> type) {
		this.configuration = configuration;
		this.type = type;
		String resource = type.getName().replace('.', '/') + ".java (best guess)";
		this.assistant = new MapperBuilderAssistant(configuration, resource);
	}

	public void parse() {
		String resource = type.toString();
		if (!configuration.isResourceLoaded(resource)) {
			loadXmlResource();
			configuration.addLoadedResource(resource);
			assistant.setCurrentNamespace(type.getName());
			parseCache();
			parseCacheRef();
			Method[] methods = type.getMethods();
			for (Method method : methods) {
				try {
					if (!method.isBridge()) { // issue #237
						parseStatement(method);
					}
				} catch (IncompleteElementException e) {
					configuration.addIncompleteMethod(new MethodResolverOvrd(this, method));
				}
			}
		}
		parsePendingMethods();
	}

	@Override
	public void parseStatement(Method method) {
		Class<?> parameterTypeClass = getParameterType(method);
		LanguageDriver languageDriver = getLanguageDriver(method);
		// 获取 SqlSource By Annt
		SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);

		if (sqlSource != null) {
			Options options = method.getAnnotation(Options.class);
			final String mappedStatementId = type.getName() + "." + method.getName();
			Integer fetchSize = null;
			Integer timeout = null;
			StatementType statementType = StatementType.PREPARED;
			ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
			SqlCommandType sqlCommandType = MapperUtils.getSqlCommandType(method);
			boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
			boolean flushCache = !isSelect;
			boolean useCache = isSelect;

			KeyGenerator keyGenerator;
			String keyProperty = "id";
			String keyColumn = null;
			if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
				// first check for SelectKey annotation - that overrides everything else
				SelectKey selectKey = method.getAnnotation(SelectKey.class);
				if (selectKey == null) {// 解析 entity 中 SelectKey
					if (SqlCommandType.INSERT.equals(sqlCommandType)) {
						try {
							Class<?> type = MapperUtils.getGenericType(mappedStatementId);
							selectKey = MapperUtils.parseSelectKey(type);
						} catch (Exception e) {
							// ignore
						}
					}
				}
				if (selectKey != null) {
					keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method),
							languageDriver);
					keyProperty = selectKey.keyProperty();
				} else {
					if (options == null) {
						keyGenerator = configuration.isUseGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
					} else {
						keyGenerator = options.useGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
						keyProperty = options.keyProperty();
						keyColumn = options.keyColumn();
					}
				}
			} else {
				keyGenerator = new NoKeyGenerator();
			}

			if (options != null) {
				flushCache = options.flushCache();
				useCache = options.useCache();
				fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
				timeout = options.timeout() > -1 ? options.timeout() : null;
				statementType = options.statementType();
				resultSetType = options.resultSetType();
			}

			String resultMapId = null;
			ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
			if (resultMapAnnotation != null) {
				String[] resultMaps = resultMapAnnotation.value();
				StringBuilder sb = new StringBuilder();
				for (String resultMap : resultMaps) {
					if (sb.length() > 0) sb.append(",");
					sb.append(resultMap);
				}
				resultMapId = sb.toString();
			} else if (isSelect) {
				resultMapId = parseResultMap(mappedStatementId, method);
			}

			MappedStatement ms = assistant.addMappedStatement(mappedStatementId, sqlSource, statementType, sqlCommandType,
					fetchSize, timeout, null, // ParameterMapID
					parameterTypeClass, resultMapId, // ResultMapID
					MapperUtils.getReturnType(mappedStatementId, method), resultSetType, flushCache, useCache, false, // TODO issue #577
					keyGenerator, keyProperty, keyColumn, null, languageDriver, null);
			
			if (sqlSource instanceof EmptySqlSource) {// TODO 关键点 setSqlSource
				sqlSource = getSqlProvideSourceFromAnnotations(ms, method);
				MapperUtils.setSqlSource(ms, sqlSource);
			}
		}
	}

	/**
	 * @Title: 获得 SqlSource
	 * @param method
	 * @param parameterType
	 * @param languageDriver
	 * @param sqlProviderAnnotationType
	 * @return
	 */
	public SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
		try {
			Class<? extends Annotation> sqlAnnotationType = AnntHelper.getSqlAnnotationType(method);
			// TODO 关键点 是否有 SqlProviderAnnotation 注解
			Class<? extends Annotation> sqlProviderAnnotationType = AnntHelper.getSqlProviderAnnotationType(method);
			if (sqlAnnotationType != null) {
				if (sqlProviderAnnotationType != null) {
					throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named "
							+ method.getName());
				}
				final String[] strings = (String[]) Annt.use(method, sqlAnnotationType).get("value");
				return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
			} else {
				if (sqlProviderAnnotationType != null) return EmptySqlSource.global;
			}
			return null;
		} catch (Exception e) {
			throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
		}
	}

	/**
	 * @Title: 获得 SqlProvideSource
	 * @param ms
	 * @param method
	 * @param sqlProviderAnnotationType
	 * @return
	 */
	public SqlSource getSqlProvideSourceFromAnnotations(MappedStatement ms, Method method) {
		try {
			// TODO 关键点 是否有 SqlProviderAnnotation 注解
			Class<? extends Annotation> sqlProviderAnnotationType = AnntHelper.getSqlProviderAnnotationType(method);
			if (sqlProviderAnnotationType != null) {
				Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
				// TODO 关键点重写 ProviderSqlSource 实现方式
				return new ProviderSqlSourceSupper(ms, sqlProviderAnnotation);
			}
			return null;
		} catch (Exception e) {
			throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
		}
	}

	public abstract void parseCache();

	public abstract void parsePendingMethods();

	public abstract void parseCacheRef();

	public abstract void loadXmlResource();

	public abstract String parseResultMap(String mappedStatementId, Method method);

	public abstract KeyGenerator handleSelectKeyAnnotation(SelectKey selectKey, String mappedStatementId,
			Class<?> parameterType, LanguageDriver languageDriver);

	public abstract LanguageDriver getLanguageDriver(Method method);

	public abstract Class<?> getParameterType(Method method);

	protected abstract SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterType,
			LanguageDriver languageDriver);
}
